//
// OpenTemplate
// Copyright (c) 2006
// by OpenArrow Software ( http://www.openarrow.com )
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 
// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions 
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
//

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection.Emit;
using System.CodeDom;
using OpenArrow.Templating.Parsing;
using OpenArrow.Templating.Directives;
using OpenArrow.Templating.Properties;
using OpenArrow.Reporting;

namespace OpenArrow.Templating.Compilation
{
    /// <summary>
    /// Compiles a parsed template into a Code Compile Unit
    /// <seealso cref="OpenArrow.Templating.Compilation.ITemplateGenerator"/>
    /// </summary>
    /// <remarks>
    /// <para>Here is a short example for using template parsers and generators:</para>
    /// <example language="C#">
    /// <code>
    /// using System;
    /// using System.IO;
    /// 
    /// using OpenArrow.Templating.Parsing;
    /// using OpenArrow.Templating.Compilation;
    ///  
    /// public class MyQuickGenerator() 
    /// {
    /// 	public static void Main(string[] args)
    /// 	{
    ///         // Create the parser and generator
    ///         ITemplateParser parser = new ASPNetStyleTemplateParser();
    ///         ITemplateGenerator generator = new StandardTemplateGenerator();
    ///         
    ///         // Open a file stream and stream reader for the template
    ///         FileStream stream = new FileStream("MyTemplate.template", FileAccess.Read);
    ///         StreamReader reader = new StreamReader(stream);
    /// 
    ///         // Parse the template
    ///         TemplateParserResults parserResults = parser.Parse(reader);
    /// 
    ///         // Check for problems
    ///         if(!parserResults.Success)
    ///         {
    ///             Console.WriteLine("Errors Occurred!");
    ///             return;
    ///         }
    /// 
    ///         // Generate a CodeCompileUnit from the template
    ///         TemplateGeneratorResults generatorResults = generator.Generate(parserResults.Output, "MyTemplate.template");
    /// 
    ///         // Check for problems
    ///         if(!parserResults.Success)
    ///         {
    ///             Console.WriteLine("Errors Occurred!");
    ///             return;
    ///         }
    /// 
    ///         // Do what you want with the Compile Unit now!
    ///     }
    /// }
    /// </code>
    /// </example>
    /// <example language="VB">Coming Soon!</example>
    /// <example language="C++">Sorry, no C++ examples are available. Use the C# example as a basis.</example>
    /// </remarks>
    public class StandardTemplateGenerator : ITemplateGenerator
    {
        #region Private Constants

        private const string ARG_OUTPUTWRITER = "output";
        private const string ARG_OUTPUTSTREAM = "output";
        private const string ARG_PROGRESS = "progress";

        private const string METHOD_WRITELINE = "WriteLine";
        private const string METHOD_WRITE = "Write";
        private const string METHOD_RENDER = "Render";

        #endregion

        #region ITemplateGenerator Members

        /// <summary>
        /// Generates source code for the template
        /// <seealso cref="OpenArrow.Templating.Compilation.ITemplateGenerator.GenerateSource(ParsedTemplate, string)"/>
        /// </summary>
        /// <param name="template">The template to generate source for</param>
        /// <param name="templateFileName">The template file name for Line Pragmas (leave Null or Empty to omit Line Pragmas)</param>
        /// <returns>The <see cref="System.CodeDom.CodeCompileUnit"/> representing the compiled template file</returns>
        /// <exception cref="System.ArgumentNullException"><paramref name="template"/> is null.</exception>
        /// <exception cref="System.ArgumentException"><paramref name="templateFileName"/> is null or empty.</exception>
        /// <exception cref="OpenArrow.Templating.ActionProblemsException">A large number of errors occurred in the operation and it had to be aborted</exception>
        public TemplateGeneratorResults GenerateSource(ParsedTemplate template, string templateFileName)
        {
            if (template == null)
                throw new ArgumentNullException(String.Format(Resources.Exception_ArgIsNull, "template"), "template");
            if (String.IsNullOrEmpty(templateFileName))
                throw new ArgumentException(String.Format(Resources.Exception_ArgIsNullOrEmpty, "templateFileName"), "templateFileName");

            return GenerateSource(template, templateFileName, new NullProgressReporter());
        }

        /// <summary>
        /// Generates source code for the template
        /// </summary>
        /// <seealso cref="OpenArrow.Templating.Compilation.ITemplateGenerator.GenerateSource(ParsedTemplate, string, IProgressReporter)"/>
        /// <remarks>This method reports its progress to a given progress reporter</remarks>
        /// <param name="template">The template to generate source for</param>
        /// <param name="templateFileName">
        /// The template file name for Line Pragmas 
        /// (set to <see langword="null"/> or <see cref="String.Empty"/> to omit Line Pragmas)
        /// </param>
        /// <param name="reporter">The progress reporter to report to, or <see langword="null"/> to indicate that progress reporting is not needed</param>
        /// <returns>The <see cref="System.CodeDom.CodeCompileUnit"/> representing the compiled template file</returns>
        /// <exception cref="System.ArgumentNullException"><paramref name="template"/> is null.</exception>
        /// <exception cref="System.ArgumentException"><paramref name="templateFileName"/> is null or empty.</exception>
        /// <exception cref="OpenArrow.Templating.ActionProblemsException">A large number of errors occurred in the operation and it had to be aborted</exception>
        public TemplateGeneratorResults GenerateSource(ParsedTemplate template, string templateFileName, IProgressReporter reporter)
        {
            if (template == null)
                throw new ArgumentNullException(String.Format(Resources.Exception_ArgIsNull, "template"), "template");
            if (String.IsNullOrEmpty(templateFileName))
                throw new ArgumentException(String.Format(Resources.Exception_ArgIsNullOrEmpty, "templateFileName"), "templateFileName");
            if (reporter == null)
                reporter = new NullProgressReporter();

            // Create the type
            TemplateGeneratorResults results = new TemplateGeneratorResults();
            CodeCompileUnit compileUnit = new CodeCompileUnit();
            CodeTypeDeclaration templateType = new CodeTypeDeclaration();

            // Execute the TemplateDirective, which will add the type to the compile unit
            using (reporter.StartTask(Resources.Task_ExecutingDirectives, template.Directives.Count + 1))
            {
                if (template.TemplateDirective == null)
                {
                    results.Problems.Add(Problem.Error_NoTemplateDirective());
                    results.Success = false;
                    return results;
                }
                template.TemplateDirective.Execute(new DirectiveContext(template, templateType, compileUnit, results.Problems, reporter));
                reporter.DoWork();


                // Build the empty namespace for imports
                compileUnit.Namespaces.Add(new CodeNamespace());

                // Execute the other directive, but if we see a TemplateDirective, then we have a problem
                foreach (IDirective directive in template.Directives)
                {
                    if (directive is TemplateDirective)
                    {
                        results.Problems.Add(Problem.Warning_MultipleTemplateDirectives());
                    }
                    else
                    {
                        directive.Execute(new DirectiveContext(template, templateType, compileUnit, results.Problems, reporter));
                        reporter.DoWork();
                    }
                }
            }

            CodeMemberMethod renderMethod = null;
            using (reporter.StartTask(Resources.Task_PreparingTemplateClass))
            {
                renderMethod = PrepareType(templateType, reporter);
            }


            // Process the segments
            using (reporter.StartTask(Resources.Task_ProcessingSegments))
            {
                foreach (Segment segment in template.Segments)
                {
                    if (segment.IsLiteral)
                        ProcessLiteralSegment(segment, renderMethod, templateFileName, reporter);
                    else
                        ProcessCodeSegment(segment, templateType, results.Problems, renderMethod, templateFileName, reporter);
                }
            }

            templateType.Members.Add(renderMethod);

            results.Success = true;
            results.Output = compileUnit;
            return results;
        }

        #endregion

        #region Private Methods

        private static void ProcessCodeSegment(Segment segment, CodeTypeDeclaration templateType, ProblemCollection problems,
            CodeMemberMethod renderMethod, string templateFileName, IProgressReporter reporter)
        {
            // Check for the various types of code segment
            CodeStatement stmt = null;
            if (segment.Contents.StartsWith("--") && segment.Contents.EndsWith("--"))
                return; // Its a comment, ignore it
            else if (segment.Contents.StartsWith("!"))
                // Declaration Segment: <%! private string myString; %> place as literal in type
                ProcessDeclarationSegment(segment, templateType, templateFileName, reporter);
            else if (segment.Contents.StartsWith("="))
                // Output Segment: <%= CodeExpression %> => output.Write(CodeExpression)
                stmt = ProcessOutputSegment(segment.Contents.Substring(1), reporter);
            else if (segment.Contents.StartsWith("$"))
                // Expression Segment: <%$ Expression: Data %> => Use 'Expression' to look up a class and pass it 'Data' to get a CodeStatement
                stmt = ProcessExpressionSegment(segment, templateType, problems, renderMethod, reporter);
            else
            {
                // Standard code segment, dump it into a CodeSnippetStatement
                using (reporter.StartTask(Resources.Task_ProcessingCodeSegment))
                {
                    stmt = new CodeSnippetStatement(segment.Contents.Trim());
                }
            }
            if (stmt != null)
            {
                stmt.LinePragma = new CodeLinePragma(templateFileName, segment.StartLine);
                renderMethod.Statements.Add(stmt);
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "renderMethod"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "templateType")]
        private static CodeStatement ProcessExpressionSegment(Segment segment, CodeTypeDeclaration templateType, ProblemCollection problems,
            CodeMemberMethod renderMethod, IProgressReporter reporter)
        {
            using (reporter.StartTask(Resources.Task_ProcessingExpressionSegment))
            {
                problems.Add(Problem.Warning_FeatureNotImplemented("Expression Segments <%$ %>", segment.StartLine));
            }
            return null;
        }

        private static CodeStatement ProcessOutputSegment(string segment, IProgressReporter reporter)
        {
            using (reporter.StartTask(Resources.Task_ProcessingOutputSegment))
            {
                return new CodeExpressionStatement(new CodeMethodInvokeExpression(
                    new CodeMethodReferenceExpression(new CodeArgumentReferenceExpression(ARG_OUTPUTWRITER), METHOD_WRITE),
                    new CodeSnippetExpression(segment.Trim())));
            }
        }

        private static void ProcessDeclarationSegment(Segment segment, CodeTypeDeclaration templateType, string templateFileName, 
            IProgressReporter reporter)
        {
            using (reporter.StartTask(Resources.Task_ProcessingDeclarationSegment))
            {
                CodeTypeMember member = new CodeSnippetTypeMember(segment.Contents.Substring(1).Trim());
                member.LinePragma = new CodeLinePragma(templateFileName, segment.StartLine);
                templateType.Members.Add(member);
            }
        }

        private static void ProcessLiteralSegment(Segment segment,
            CodeMemberMethod renderMethod, string templateFileName, IProgressReporter reporter)
        {
            using (reporter.StartTask(Resources.Task_ProcessingLiteralSegment))
            {
                // Find the first \n
                int lineEnd = -1;
                int startIndex = 0;
                int linePragmaLine = segment.StartLine;
                while (startIndex < segment.Contents.Length && (lineEnd = segment.Contents.IndexOf('\n', startIndex)) != -1)
                {
                    // Take everything up to the \n
                    string subsegment = segment.Contents.Substring(startIndex, lineEnd - startIndex);
                    AddOutputCall(renderMethod, subsegment, METHOD_WRITELINE, templateFileName, linePragmaLine);
                    startIndex = lineEnd + 1;
                    linePragmaLine++;
                }
                if (startIndex < segment.Contents.Length)
                    // No line ending, use Write
                    AddOutputCall(renderMethod, segment.Contents.Substring(startIndex), METHOD_WRITE, templateFileName, linePragmaLine);
            }
        }

        private static void AddOutputCall(CodeMemberMethod targetMethod, string text, string method, string templateFileName,
            int linePragmaLine)
        {
            CodeExpressionStatement stmt = null;
            if (String.IsNullOrEmpty(text))
                stmt = new CodeExpressionStatement(new CodeMethodInvokeExpression(
                            new CodeMethodReferenceExpression(new CodeArgumentReferenceExpression(ARG_OUTPUTWRITER), method)));
            else
                stmt = new CodeExpressionStatement(new CodeMethodInvokeExpression(
                            new CodeMethodReferenceExpression(new CodeArgumentReferenceExpression(ARG_OUTPUTWRITER), method),
                            new CodePrimitiveExpression(text)));
            stmt.LinePragma = new CodeLinePragma(templateFileName, linePragmaLine);
            targetMethod.Statements.Add(stmt);
        }

        private static CodeMemberMethod PrepareType(CodeTypeDeclaration templateType, IProgressReporter reporter)
        {
            using (reporter.StartTask(Resources.Task_CreatingType))
            {
                templateType.IsClass = true;
                templateType.Attributes = MemberAttributes.Public;
                templateType.BaseTypes.Add(new CodeTypeReference(typeof(ITemplate)));
                templateType.Comments.Add(new CodeCommentStatement(Resources.Comment_TemplateHeader, true));
            }

            // Build Render(Stream) method
            using (reporter.StartTask(Resources.Task_BuildingRenderToStream))
            {
                CodeMemberMethod renderStreamMethod = new CodeMemberMethod();
                renderStreamMethod.Attributes = MemberAttributes.Public;
                renderStreamMethod.Comments.Add(new CodeCommentStatement(Resources.Comment_RenderStreamMethodHeader, true));
                renderStreamMethod.ImplementationTypes.Add(typeof(ITemplate));
                renderStreamMethod.Name = METHOD_RENDER;
                renderStreamMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(Stream), ARG_OUTPUTSTREAM));
                renderStreamMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(IProgressReporter), ARG_PROGRESS));
                renderStreamMethod.Statements.Add(new CodeMethodInvokeExpression(
                    new CodeMethodReferenceExpression(new CodeThisReferenceExpression(), METHOD_RENDER),
                    new CodeObjectCreateExpression(typeof(StreamWriter), new CodeArgumentReferenceExpression(ARG_OUTPUTSTREAM)),
                    new CodeArgumentReferenceExpression(ARG_PROGRESS)));
                templateType.Members.Add(renderStreamMethod);
            }

            // Build Render(TextWriter) method
            CodeMemberMethod renderWriterMethod = null;
            using (reporter.StartTask(Resources.Task_BuildingRenderToTextWriter))
            {
                renderWriterMethod = new CodeMemberMethod();
                renderWriterMethod.Attributes = MemberAttributes.Public;
                renderWriterMethod.Comments.Add(new CodeCommentStatement(Resources.Comment_RenderWriterMethodHeader, true));
                renderWriterMethod.ImplementationTypes.Add(typeof(ITemplate));
                renderWriterMethod.Name = METHOD_RENDER;
                renderWriterMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(TextWriter), ARG_OUTPUTWRITER));
                renderWriterMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(IProgressReporter), ARG_PROGRESS));
            }
            return renderWriterMethod;
        }

        #endregion
    }
}
